/*
 * SqlSyntaxBuilder.scala
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.squela
package sql

class SqlSyntaxBuilder(val syntaxProvider: SqlSyntaxProvider) {

  type RawField[T] = org.squela.Field[T]

  def build(query: Query): String = {
    val res = new StringBuilder
    build1(query, res)
    res.toString
  }

  private def build1(implicit query: Query, sb: StringBuilder) {
    @inline def addWhere {
      if(query.whereAssociations != null) {
        sb append " "
        sb append "WHERE "
        buildLogicalLink1(query.whereAssociations, true)
      }
    }

    query.queryType match {
      case s: Select[_] =>
        sb append "SELECT "
        if(s.fields.isEmpty) sb append "*"
        else sb append (s.fields.map{field => buildFieldName(field)}.mkString(", "))
        sb append " FROM "
        sb append query.resource.name
        addWhere
      case i: Insert[_] =>
        sb append "INSERT INTO "
        if(!query.resource.isInstanceOf[Table]) throw new IllegalArgumentException("Only instances of Table are accepted in INSERT")
        sb append query.resource.asInstanceOf[Table].name
        sb append " ("
        sb append (i.associations map (a => buildFieldName(a.field)) mkString ", ")
        sb append ") VALUES("
        i.associations foreach {a => syntaxProvider.syntaxValue(a.value, sb); sb append ", "}
        sb.setLength(sb.length - 2)
        sb append ")"
      case u : Update[_] =>
        if(!query.resource.isInstanceOf[Table]) throw new IllegalArgumentException("Only instances of Table are accepted in UPDATE")
        if(u.associations.isEmpty) throw new IllegalArgumentException("Set associations cannot be empty")
        sb append "UPDATE "
        sb append query.resource.asInstanceOf[Table].name
        sb append " SET "
        u.associations foreach {assoc => buildAssociation1(assoc, false); sb append ", "}
        sb.setLength(sb.length - 2)
        addWhere
      case d @ Delete() =>
        if(!query.resource.isInstanceOf[Table]) throw new IllegalArgumentException("Only instances of Table are accepted in DELETE")
        sb append "DELETE FROM "
        sb append query.resource.asInstanceOf[Table].name
        addWhere
      case c : Create[_] =>
        if(!query.resource.isInstanceOf[Table]) throw new IllegalArgumentException("Only instances of Table are accepted in CREATE")
        val table = query.resource.asInstanceOf[Table]
        sb append ("CREATE TABLE " + table.name)
        if(!c.fields.isEmpty) {
          sb append "("
          syntaxProvider.syntaxTableFieldsCreate(c.fields, sb)
          sb append ")"
        }
      case alter: AlterResource =>
        if(!query.resource.isInstanceOf[Table]) throw new IllegalArgumentException("Only instances of Table are accepted in ALTER TABLE")
        sb append "ALTER TABLE " + query.resource.asInstanceOf[Table] + " "
        alter match {
          case AddColumn(field) => sb append "ADD "; syntaxProvider.syntaxTableFieldCreate(field, sb)
          case DropColumn(field) => sb append "DROP " + field.name
          case ChangeColumnName(oldName, newColumn) => sb append "CHANGE " + oldName + " " + newColumn.name + " "; syntaxProvider.syntaxType(newColumn, sb)
          case ChangeColumnType(column) => sb append "MODIFY " + column.name + " "; syntaxProvider.syntaxType(column, sb)
          case AddPrimaryKey(pk) => sb append "ADD PRIMARY KEY (" + pk.name + ")"
          case AddForeignKey(fk) => sb append "ADD FOREIGN KEY (" + fk.resource.name + "_" + fk.name + ") REFERENCES " + fk.referencedTable.name + "(" + fk.field.name + ")"
        }
    }
  }

  private def buildFieldName(field: RawField[_])(implicit query: Query) = {
    var res = field match {
      case f: SqlField[_] => f.reference.getOrElse(f).name
      case other => field.name
    }
    if(query.resource != null && !query.resource.isInstanceOf[Table]) {
      if(field.resource != null) res = (field.resource.asInstanceOf[Table].alias + ".") + res
      else throw new IllegalStateException("Field " + field.name + " is detached and used in a join query")
    }
    if(field.isInstanceOf[SqlFieldOperation[_]]) res = field.asInstanceOf[SqlFieldOperation[_]](res)
    res
  }

  def buildLogicalLink(link: LogicalLink, inWhere: Boolean): String = {
    val res = new StringBuilder
    val q = new Query
    q.where(link)
    buildLogicalLink1(link, inWhere)(res, q)
    res.toString
  }

  private def buildLogicalLink1(link: LogicalLink, inWhere: Boolean)(implicit sb: StringBuilder, query: Query) {
    link match {
      case assoc: FieldAssociation =>
        buildAssociation1(assoc, inWhere)
      case logicalOp: LogicalOp =>
        @inline def appendOp(a: LogicalLink, op: String, b: LogicalLink ) {
          buildLogicalLink1(a, inWhere)
          sb append op
          if(b.isInstanceOf[LogicalOp]){
            sb append "("
            buildLogicalLink1(b, inWhere)
            sb append ")"
          } else buildLogicalLink1(b, inWhere)
        }
        logicalOp match {
          case a&&b => appendOp(a, " AND ", b)
          case a||b=> appendOp(a, " OR ", b)
        }
    }
  }

  def buildAssociation(assoc: FieldAssociation, inWhere: Boolean): String = {
    val res = new StringBuilder
    val q = new Query
    buildAssociation1(assoc, inWhere)(res, q)
    res.toString
  }

  private def buildAssociation1(assoc: FieldAssociation, inWhere: Boolean)(implicit sb: StringBuilder, query: Query) {
    assoc match {
      case l @ fld Like str =>
        sb append fld.name
        sb append " "
        syntaxProvider.syntaxAssociation(l, inWhere, sb)
        sb append " '"
        sb append str
        sb append "'"
      case l @ fld In query =>
        sb append fld.name
        sb append " "
        syntaxProvider.syntaxAssociation(l, inWhere, sb)
        sb append "("
        build1(query, sb)
        sb append ")"
      case other =>
        sb append buildFieldName(other.field) append " "
        syntaxProvider.syntaxAssociation(other, inWhere, sb)
        sb append " "
        syntaxProvider.syntaxValue(other.value, sb)
    }
  }
}
